/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.util;

import java.beans.*;
import java.lang.ref.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import ch.trackedbean.common.*;

/**
 * Cache holding property descriptors.
 * 
 * @author M. Hautle
 */
public class BeanUtils {
	/** Map caching the properties of used classes. The key is built from the specification of the desired property. */
	private static Map<Key, Reference<PropertyDescriptor>> PROPERTIES = new ConcurrentHashMap<Key, Reference<PropertyDescriptor>>();

	/**
	 * Returns the descriptor of a given property or null if it does not exists.
	 * 
	 * @param type The type
	 * @param property The property name
	 * @return The descriptor of the given property or null
	 * @throws TechnicalException If something goes wrong
	 */
	public static PropertyDescriptor getDescriptor(Class type, String property) throws TechnicalException {
		final Key key = new Key(type, property);
		final Reference<PropertyDescriptor> ref = PROPERTIES.get(key);
		final PropertyDescriptor desc = ref != null ? ref.get() : null;
		if (desc != null)
			return desc;
		try {
			for (PropertyDescriptor p : Introspector.getBeanInfo(type).getPropertyDescriptors()) {
				if (property.equals(p.getName())) {
					PROPERTIES.put(key, new SoftReference<PropertyDescriptor>(p));
					return p;
				}
			}
		} catch (IntrospectionException e) {
			throw new TechnicalException("Error during property lookup of " + property + " in " + type.getName(), e);
		}
		return null;
	}

	/**
	 * Returns the getter of a given property or null if it does not exists.
	 * 
	 * @param type The type
	 * @param property The property name
	 * @return The getter of the given property or null
	 * @throws TechnicalException If something goes wrong
	 */
	public static Method getGetter(Class type, String property) {
		final PropertyDescriptor desc = getDescriptor(type, property);
		if (desc == null)
			return null;
		return desc.getReadMethod();
	}

	/**
	 * Returns the setter of a given property or null if it does not exists.
	 * 
	 * @param type The type
	 * @param property The property name
	 * @return The setter of the given property or null
	 * @throws TechnicalException If something goes wrong
	 */
	public static Method getSetter(Class type, String property) {
		final PropertyDescriptor desc = getDescriptor(type, property);
		if (desc == null)
			return null;
		return desc.getWriteMethod();
	}

	/**
	 * Creates a map holding the {@link PropertyDescriptor}s (key=name) of the given class.
	 * 
	 * @param type The type
	 * @return A map holding the properties
	 * @throws TechnicalException If something goes wrong
	 */
	public static Map<String, PropertyDescriptor> getProperties(Class type) throws TechnicalException {
		try {
			final Map<String, PropertyDescriptor> props = new HashMap<String, PropertyDescriptor>();
			for (PropertyDescriptor p : Introspector.getBeanInfo(type).getPropertyDescriptors())
				props.put(p.getName(), p);
			return props;
		} catch (IntrospectionException e) {
			throw new TechnicalException("Error while getting property list from " + type.getName(), e);
		}
	}

	/**
	 * Creates a flat copy of the given bean.<br>
	 * If the given object is not a bean (so something like immutable) the passed instance will be returned.
	 * 
	 * @param <T> The type
	 * @param bean The bean to copy a immutable object or null
	 * @return A copy, the instance itself or null
	 */
	@SuppressWarnings("unchecked")
	public static <T> T copy(T bean) {
		if (bean == null)
			return null;
		try {
			final Class<? extends Object> type = bean.getClass();
			final PropertyDescriptor[] props = Introspector.getBeanInfo(type).getPropertyDescriptors();
			int i = 0;
			Method readM = null;
			Method writeM = null;
			// search the first writable property
			for (; i < props.length; i++) {
				if (props[i] instanceof IndexedPropertyDescriptor)
					continue;
				readM = props[i].getReadMethod();
				writeM = props[i].getWriteMethod();
				if (readM != null && writeM != null)
					break;
			}
			// return the original bean if no writable property was found
			if (readM == null || writeM == null)
				return bean;
			// clone current bean
			final Object copy = type.newInstance();
			// clone first property
			writeM.invoke(copy, readM.invoke(bean));
			// clone the other properties
			for (i++; i < props.length; i++) {
				if (props[i] instanceof IndexedPropertyDescriptor)
					continue;
				readM = props[i].getReadMethod();
				writeM = props[i].getWriteMethod();
				if (readM != null && writeM != null)
					writeM.invoke(copy, readM.invoke(bean));
			}
			return (T) copy;
		} catch (IntrospectionException e) {
			throw new TechnicalException("Error while getting property list from " + bean.getClass().getName(), e);
		} catch (Exception e) {
			throw new TechnicalException("Error while copying properties", e);
		}
	}

	/**
	 * Copies the values from a source bean to a destination bean (of the same type).<br>
	 * This is just a 'flat' copy, use {@link BeanCloner} to create a deep copy.
	 * 
	 * @param <T> The type
	 * @param src The source bean
	 * @param dst The destination bean
	 * @return The destination bean
	 * @throws TechnicalException If something went wrong
	 */
	public static <T> T copyValues(T src, T dst) throws TechnicalException {
		try {
			for (PropertyDescriptor p : Introspector.getBeanInfo(src.getClass()).getPropertyDescriptors()) {
				final Method read = p.getReadMethod();
				final Method write = p.getWriteMethod();
				if (read != null && write != null)
					write.invoke(dst, read.invoke(src));
			}
			return dst;
		} catch (IntrospectionException e) {
			throw new TechnicalException("Error while getting property list from " + src.getClass().getName(), e);
		} catch (Exception e) {
			throw new TechnicalException("Error while copying properties", e);
		}
	}

	/**
	 * Copies the values from a source bean to a destination bean (of different type).<br>
	 * This is just a 'flat' copy of properties with the same name.
	 * 
	 * @param <S> The source type
	 * @param <T> The destination type
	 * @param src The source bean
	 * @param dst The destination bean
	 * @param ignore The properties to ignore
	 * @return The destination bean
	 * @throws TechnicalException If something went wrong
	 */
	public static <S, T> T copyValuesTo(S src, T dst, String... ignore) throws TechnicalException {
		try {
			final Set<String> exclude = toSet(ignore);
			final Map<String, PropertyDescriptor> dstProps = getProperties(dst.getClass());
			for (PropertyDescriptor p : Introspector.getBeanInfo(src.getClass()).getPropertyDescriptors()) {
				final String name = p.getName();
				if (exclude.contains(name))
					continue;
				final PropertyDescriptor dProp = dstProps.get(name);
				if (dProp == null)
					continue;
				final Method read = p.getReadMethod();
				final Method write = dProp.getWriteMethod();
				if (read != null && write != null)
					write.invoke(dst, read.invoke(src));
			}
			return dst;
		} catch (IntrospectionException e) {
			throw new TechnicalException("Error while getting property list from " + src.getClass().getName(), e);
		} catch (Exception e) {
			throw new TechnicalException("Error while copying properties", e);
		}
	}

	/**
	 * Converts the given array into a set.
	 * 
	 * @param <T> The type
	 * @param v The values
	 * @return A set containing the given valuse
	 */
	public static <T> Set<T> toSet(T... v) {
		if (v.length == 0)
			return Collections.emptySet();
		final Set<T> s = new HashSet<T>();
		Collections.addAll(s, v);
		return s;
	}

	/**
	 * Expandes the given array by the specified element count.
	 * 
	 * @param <T> The content type
	 * @param array The array
	 * @param size The size to add
	 * @return The 'resized' array
	 */
	@SuppressWarnings("unchecked")
	public static <T> T[] arrayExpand(T[] array, int size) {
		final Class<?> type = array.getClass().getComponentType();
		final T[] dst = (T[]) Array.newInstance(type, size + array.length);
		System.arraycopy(array, 0, dst, 0, array.length);
		return dst;
	}

	/**
	 * Adds the passed element to the given array.
	 * 
	 * @param <T> The content type
	 * @param array The array
	 * @param value The value to add
	 * @return The new array
	 */
	public static <T> T[] arrayAppend(T[] array, T value) {
		final T[] dst = arrayExpand(array, 1);
		dst[array.length] = value;
		return dst;
	}

	/**
	 * Adds the passed elements to the given array.
	 * 
	 * @param <T> The content type
	 * @param array The array
	 * @param values The values to add
	 * @return The new array
	 */
	public static <T> T[] arrayAppend(T[] array, T... values) {
		final T[] dst = arrayExpand(array, values.length);
		System.arraycopy(values, 0, dst, array.length, values.length);
		return dst;
	}

	/**
	 * Clears the cache.
	 */
	public static void clearCache() {
		PROPERTIES.clear();
		PROPERTIES = new ConcurrentHashMap<Key, Reference<PropertyDescriptor>>();
	}

	/**
	 * Object representing an key.
	 * 
	 * @author M. Hautle
	 */
	private static final class Key {
		/** The type holding the property. */
		private final Class type;

		/** The name of the property. */
		private final String property;

		/** The hash of this key. */
		private final int hash;

		/**
		 * Default constructor.
		 * 
		 * @param type The type holding the property
		 * @param property The property name
		 */
		Key(Class type, String property) {
			this.type = type;
			this.property = property;
			this.hash = 31 * (31 + property.hashCode()) + type.hashCode();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public int hashCode() {
			return hash;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public boolean equals(Object obj) {
			final Key o = ((Key) obj);
			return type.equals(o.type) && property.equals(o.property);
		}
	}
}
